import { _decorator, Component, Vec3 } from 'cc';

const { ccclass, property } = _decorator;

function clampToPositiveInt(value: number, fallback: number): number {
  if (!Number.isFinite(value) || value <= 0) return fallback;
  return Math.max(1, Math.round(value));
}

@ccclass('TileOccluder')
export class TileOccluder extends Component {
  static instance: TileOccluder | null = null;

  @property
  public cellSize = 64; // world units per tile

  @property
  public chunkSize = 512; // world units per chunk

  private tilesPerChunk = 8;
  private occupiedTiles: Set<string> = new Set();
  private chunkTileRefs: Map<string, Set<string>> = new Map();

  onDestroy() {
    if (TileOccluder.instance === this) TileOccluder.instance = null;
  }

  public refreshDerived() {
    this.tilesPerChunk = clampToPositiveInt(this.chunkSize / this.cellSize, this.tilesPerChunk || 8);
  }

  private chunkKey(cx: number, cy: number): string {
    return `${cx}:${cy}`;
  }

  private tileKeyFromGlobal(gx: number, gy: number): string {
    return `${gx},${gy}`;
  }

  public clearChunk(cx: number, cy: number): void {
    const cKey = this.chunkKey(cx, cy);
    const tiles = this.chunkTileRefs.get(cKey);
    if (!tiles) return;
    for (const tile of tiles) {
      this.occupiedTiles.delete(tile);
    }
    this.chunkTileRefs.delete(cKey);
  }

  public setBlocked(cx: number, cy: number, tx: number, ty: number, blocked: boolean): void {
    this.refreshDerived();
    const tilesPer = this.tilesPerChunk;
    const gx = cx * tilesPer + tx;
    const gy = cy * tilesPer + ty;
    const tileKey = this.tileKeyFromGlobal(gx, gy);
    const cKey = this.chunkKey(cx, cy);
    if (blocked) {
      this.occupiedTiles.add(tileKey);
      let bag = this.chunkTileRefs.get(cKey);
      if (!bag) {
        bag = new Set();
        this.chunkTileRefs.set(cKey, bag);
      }
      bag.add(tileKey);
    } else {
      this.occupiedTiles.delete(tileKey);
      const bag = this.chunkTileRefs.get(cKey);
      if (bag) {
        bag.delete(tileKey);
        if (bag.size === 0) {
          this.chunkTileRefs.delete(cKey);
        }
      }
    }
  }

  public isBlockedWorld(pos: Vec3): boolean {
    if (this.cellSize <= 0) return false;
    const gx = Math.floor(pos.x / this.cellSize);
    const gy = Math.floor(pos.y / this.cellSize);
    return this.occupiedTiles.has(this.tileKeyFromGlobal(gx, gy));
  }

  /** Bresenham traversal on tile grid (world snapped to cellSize). */
  public hasLineOfSight(a: Vec3, b: Vec3): boolean {
    if (this.cellSize <= 0) return true;
    const startX = Math.floor(a.x / this.cellSize);
    const startY = Math.floor(a.y / this.cellSize);
    const endX = Math.floor(b.x / this.cellSize);
    const endY = Math.floor(b.y / this.cellSize);

    let x0 = startX;
    let y0 = startY;
    const x1 = endX;
    const y1 = endY;
    const dx = Math.abs(x1 - x0);
    const dy = Math.abs(y1 - y0);
    const sx = x0 < x1 ? 1 : -1;
    const sy = y0 < y1 ? 1 : -1;
    let err = dx - dy;

    const tileIsBlocked = (tx: number, ty: number): boolean => {
      // Allow origin/target tiles – consider them visible by design.
      if ((tx === startX && ty === startY) || (tx === endX && ty === endY)) {
        return false;
      }
      return this.occupiedTiles.has(this.tileKeyFromGlobal(tx, ty));
    };

    while (true) {
      if (tileIsBlocked(x0, y0)) {
        return false;
      }
      if (x0 === x1 && y0 === y1) {
        break;
      }
      const e2 = err * 2;
      if (e2 > -dy) {
        err -= dy;
        x0 += sx;
      }
      if (e2 < dx) {
        err += dx;
        y0 += sy;
      }
    }
    return true;
  }

  static get(): TileOccluder | null {
    return TileOccluder.instance;
  }
}

export default TileOccluder;

export function bootstrapTileOccluder(occluder: TileOccluder) {
  console.log('[OCC] bootstrap...');
  TileOccluder.instance = occluder;
  occluder.refreshDerived();
}
